iT邦幫忙

2024 iThome 鐵人賽

DAY 4
0
Python

Python 錦囊密技系列 第 4

【Python錦囊㊙️技4】函數式程式設計(Functional Programming)

  • 分享至 

  • xImage
  •  

前言

函數式程式設計(Functional Programming)是一種設計模式(Design pattern),主程式能夠將函數當作參數,進行傳遞(輸出/輸入參數)及評估。使用Functional Programming的好處如下:

  1. 模組化(Modularity):程式架構不需改變,可以無限擴充功能,後續會詳細說明。
  2. 可利用高階函數(Higher-order functions)取代迴圈。
  3. 黑箱(Transparent):使用純函數只需要瞭解輸出/輸入參數,不必瞭解細節。
  4. 有利於平行處理(Parallelizable)的實作:因為純函數無狀態、不可變,執行緒之間互相獨立。
  5. 易於優化及重構(Refactoring):因為純函數(Pure function)易於修改,優化及重構的困難度較低。

實作

可實作Functional Programming的語言必須支援First-class function,意思是函數可被視為普通數值,可被儲存於變數,可被其他函數當作參數傳遞並被執行,Python完全符合這些要求。以下我們就從頭開發(From scratch),展現Functional Programming的威力。

範例1. 實作加減乘除四則運算,檔案名稱為fp1.py。

  1. 定義加減乘除四則運算。
def add(a, b):
    return a+b

def subtract(a, b):
    return a-b

def multiply(a, b):
    return a*b

def divide(a, b):
    return a/b
  1. 測試:直接呼叫。
subtract(5,10)
  1. 執行結果無誤:-5。

  2. 加寫一個函數perform1,可接受加減乘除函數作為輸入參數。

def perform1(func, a, b ):
    return func(a, b)
  1. 測試:呼叫perform1,subtract作為輸入參數。
print(perform1(subtract, 5, 10))
  1. 執行結果無誤:-5。

這樣有甚麼好處呢? 假設有一天我們想要擴充功能,希望程式也能夠進行指數及log運算,這時原先的程式都不必修改,只要加指數及log運算函數即可。

  1. 增加指數及log運算函數。
import math
...
def exp(a, b):
    return a**b

def log(a, b):
    return math.log(a, b)
  1. 測試:呼叫perform1,log作為輸入參數。
print(perform1(log, 100, 10))
  1. 執行結果無誤:log10(100)=2.0。

範例2. 程式也可以將參數a、b改為*arg,可以接受任意個參數,與print函數類似,參看fp2.py。

def add(*arg):
    total = 0
    for i in arg:
        total += i
    return total

def multiply(*arg):
    total = 1
    for i in arg:
        total *= i
    return total

def perform1(func, *arg):
    return func(*arg)
    
if __name__ == "__main__":
    print(perform1(multiply, 1, 10, 20))

執行結果無誤:200。

高階函數(Higher-order functions, HOF)

Python內建許多高階函數,例如上一篇談到的map、filter、reduce,另外,許多套件也建構高階函數,供開發者使用,例如Pandas的apply、applymap...等。

範例3. 使用reduce實作範例2功能。

  1. 實作相加的函數:使用匿名函數(Lambda function),也可以使用一般函數。
from functools import reduce

# 測試資料
numbers = (1, 2, 3, 4)

# 連加
result = reduce(lambda x, y: x + y, numbers)
print(result)
  1. 執行結果無誤:10。

  2. 實作連乘函數:Lambda function改為乘號(*)即可。

# 連乘
result = reduce(lambda x, y: x * y, numbers)
print(result)
  1. 執行結果無誤:24。

搭配匿名函數

匿名函數(Lambda function)是沒有名稱的函數,語法較簡潔,同時也有利於將函數以參數傳遞,例如【Python錦囊㊙️技2】的小型計算機程式,在指定button的事件處理時,可以使用下列語法:

# 建立按鈕
button1 = Button(app, text=' 1 ', fg='black', bg='lightblue', 
                command=lambda: press(1), height=1, width=7) 

command屬性指定button的事件處理函數為press(1),若刪除【lambda:】,程式會在建立按鈕時,就馬上執行press(1)。

以下改寫add函數為匿名函數。

def add(a, b):
    return a+b

改為:

add = lambda a, b: a+b

範例4. 使用匿名函數改寫fp1.py,另存為fp_lambda.py。

import math

add = lambda a, b: a+b
subtract = lambda a, b: a-b
multiply = lambda a, b: a*b
divide = lambda a, b: a/b
exp = lambda a, b: a**b
log = lambda a, b: math.log(a, b)

def perform1(func, a, b ):
    return func(a, b)
    
if __name__ == "__main__":
    #  print(subtract(5, 10))
    print(perform1(subtract, 5, 10))
    print(perform1(log, 100, 10))

如果函數只使用一次,也可以直接在呼叫時直接撰寫匿名函數,不須事先定義。

print(perform1(lambda a, b: math.log(a, b), 1000, 10))

範例5. Python提供內建模組operator,對常用的運算都已定義成函數,可直接使用,可參閱Python官方文件,如下圖:
https://ithelp.ithome.com.tw/upload/images/20240917/20001976KAJTUYkBNt.png

測試如下:

from operator import add, sub, mul, truediv

def perform1(func, a, b ):
    return func(a, b)

print(perform1(add, 100, 10))

執行結果:110。

序列化(Serialization)存檔

前面講過Python支援First-class function,函數可被儲存,之後可隨時取用。

範例6. 函數儲存與載入。

from operator import add, sub, mul, truediv
import joblib

# 建立+-*/對應add, sub, mul, truediv函數的字典
operators = list('+-*/')
func_list = [add, sub, mul, truediv]
operators_dict = {operators[i]: func_list[i] for i in range(len(operators))}

# 序列化(Serialization)存檔
joblib.dump(operators_dict, 'operator.joblib')

# 反序列化(Deserialization),自檔案載入
operators_dict2 = joblib.load('operator.joblib')

def perform_operation(op_string, a, b):
    return operators_dict2[op_string](a, b)

print(perform_operation('+', 100, 10))
  1. 建立+-*/對應add, sub, mul, truediv函數的字典。
  2. 使用 joblib 套件進行序列化(Serialization)及反序列化(Deserialization),安裝指令為pip install joblib。
  3. 可直接使用運算符號+-*/ 呼叫對應的函數。
perform_operation('+', 100, 10)

有兩個方面可以延伸:

  1. 序列化與載入原始碼有何差異? 序列化可適度保護原始碼,可再加密序列化後的檔案,就不容易曝露原始碼。
  2. 有何用途? 可使用解析器(Parser),自建特定領域的程式語言,類似許多財經分析軟體,都會提供使用者,自訂程式邏輯,找尋買賣訊號。

Pandas 高階函數

表格處理套件Pandas也提供許多高階函數(HOF),例如apply,用於轉換欄位值,以至證交所網站下載股價為例,通常要整理成可分析的表格,需要費一番手腳。
範例7. 每日收盤行情下載,檔案為【每日收盤行情下載.ipynb】,節錄重要程式如下:

  1. 下載2024/09/13收盤行情。
# 引進相關套件
import requests
from io import StringIO
import pandas as pd
import numpy as np

# 資料日期
date1 = '20240913'
# 網址
url= 'http://www.twse.com.tw/exchangeReport/MI_INDEX?response=csv&date={}&type=ALL'.format(date1)

# 送出要求,並取得回應資料
response = requests.get(url)
  1. 清除不必要的資料。
clean_data=[]
for row in response.text.split('\n'):
    fields=row.split('",')
    if len(fields) == 17 and row[0] != '=':
        clean_data.append(row.replace(' ',''))

csv_data = "\n".join(clean_data)
  1. 整理成可分析的表格(DataFrame)。
df = pd.read_csv(StringIO(csv_data))

執行結果:
https://ithelp.ithome.com.tw/upload/images/20240917/20001976MWei6hDZtz.png

  1. 【漲跌價差】應由【漲跌(+/-)】、【漲跌價差】兩個欄位合併,使用apply高階函數處理,r代表列(row)。
# 計算漲跌價差
df["漲跌價差"] = df.apply(lambda r: 0-r["漲跌價差"] if r["漲跌(+/-)"] =='-' else r["漲跌價差"], axis=1)
  1. 有些欄位應轉為數值,使用map高階函數處理。
numeric_columns=['成交股數','成交筆數','成交金額','開盤價','最高價','最低價','收盤價', '本益比']
for i in numeric_columns:
    df[i]=df[i].map(lambda x:x.replace(',', '').replace('--', '')) # 去除三位一撇、--
    df[i]=pd.to_numeric(df[i])

執行結果:
https://ithelp.ithome.com.tw/upload/images/20240917/20001976IZF2EMOOv1.png

透過Pandas高階函數可以很輕易地整理好表格資料。

純函數

被呼叫的函數最好是純函數(Pure function),具備以下特性,以避免產生副作用(side effects):

  1. 函數輸出須完全自函數輸入參數及程式邏輯推衍出來的,不含全局或外部變數。
  2. 無狀態(Stateless):任何時候呼叫,結果都一樣,寫網頁時常使用session/cookies變數,每次呼叫時都可能得到不同的值,稱之為【具狀態的】(Stateful)。
  3. 不可變的(Immutable):變數一旦初始化(Initialize),就不會再被修改。

範例8. 純函數(Pure function) vs. 不純函數(Impure function),不純函數使用了全局變數 strip_impure_call_count。

# 純函數(Pure function)
def strip_pure(sentence: str) -> str:
    return sentence.strip()


# 不純函數(Impure function)
strip_impure_call_count = 0

def strip_impure(sentence: str) -> str:
    global strip_impure_call_count

    stripped_sentence = sentence.strip()
    
    # Side effect 1: 使用全局變數 strip_impure_call_count
    strip_impure_call_count += 1
    # Side effect 2: 輸出至螢幕(stdout)
    print(f"Called strip_impure {strip_call_count} times")
    return stripped_sentence    

註:程式碼來自【Functional Programming in Python】

結語

從以上的實作,我們大致上知道Functional Programming可以讓系統架構變得非常有彈性,擴充功能,不需修改既有的程式碼,對於已上線的系統且需求變更持續頻繁的狀況,Functional Programming架構可以保持維運的穩定性,因為既有的程式碼未異動,所以也不需重新測試,只要測試新增的功能,對於中大系統開發而言,簡直是一大福音。

下次我們進一步應用Functional Programming及AST(Abstract Syntax Tree)製作一個數學式評估器(Mathematical evaluator)以及更複雜的程式解析器(Parser),敬請期待嘍。

本系列的程式碼會統一放在GitHub,本篇的程式放在src/4資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。


上一篇
【Python錦囊㊙️技3】One liners
下一篇
【Python錦囊㊙️技5】來寫一個直譯器(Interpreter)吧!
系列文
Python 錦囊密技30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言